// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using SnapObjects.Commons;

namespace SnapObjects.Data.SqlServer
{
    public class SqlServerTypeMapper : TypeMapper
    {
        private readonly SqlServerStringTypeMapping _internalString;
        private readonly SqlServerStringTypeMapping _xml = new SqlServerStringTypeMapping("xml", sqlDbType: SqlDbType.Xml, unicode: true);
        private readonly SqlServerStringTypeMapping _ntext = new SqlServerStringTypeMapping("ntext", sqlDbType: SqlDbType.NText, unicode: true);
        private readonly SqlServerStringTypeMapping _text = new SqlServerStringTypeMapping("text", sqlDbType: SqlDbType.Text, unicode: true);
        private readonly SqlServerStringTypeMapping _unboundedUnicodeString = new SqlServerStringTypeMapping("nvarchar(max)", unicode: true);
        private readonly SqlServerStringTypeMapping _keyUnicodeString = new SqlServerStringTypeMapping("nvarchar(450)", unicode: true, size: 450);
        private readonly SqlServerStringTypeMapping _unboundedAnsiString = new SqlServerStringTypeMapping("varchar(max)");
        private readonly SqlServerStringTypeMapping _keyAnsiString = new SqlServerStringTypeMapping("varchar(900)", unicode: false, size: 900);
        private readonly SqlServerStringTypeMapping _fixedLengthUnicodeString = new SqlServerStringTypeMapping("nchar", unicode: true);
        private readonly SqlServerStringTypeMapping _variableLengthUnicodeString = new SqlServerStringTypeMapping("nvarchar", unicode: true);
        private readonly SqlServerStringTypeMapping _fixedLengthAnsiString = new SqlServerStringTypeMapping("char");
        private readonly SqlServerStringTypeMapping _variableLengthAnsiString = new SqlServerStringTypeMapping("varchar");

        private readonly SqlServerByteArrayTypeMapping _variableLengthBinary = new SqlServerByteArrayTypeMapping("varbinary");
        private readonly SqlServerByteArrayTypeMapping _fixedLengthBinary = new SqlServerByteArrayTypeMapping("binary");
        private readonly SqlServerByteArrayTypeMapping _hierarchyidBinary = new SqlServerByteArrayTypeMapping("hierarchyid");
        private readonly SqlServerByteArrayTypeMapping _unboundedBinary = new SqlServerByteArrayTypeMapping("varbinary(max)");

        private readonly SqlServerByteArrayTypeMapping _keyBinary
            = new SqlServerByteArrayTypeMapping("varbinary(900)", dbType: DbType.Binary, size: 900);

        private readonly SqlServerByteArrayTypeMapping _rowversion
            = new SqlServerByteArrayTypeMapping("rowversion", dbType: DbType.Binary, size: 8);

        private readonly TimeSpanTypeMapping _time = new SqlServerTimeSpanTypeMapping("time");
        private readonly SqlServerDateTimeTypeMapping _date = new SqlServerDateTimeTypeMapping("date", dbType: DbType.Date);
        private readonly SqlServerDateTimeTypeMapping _datetime = new SqlServerDateTimeTypeMapping("datetime", dbType: DbType.DateTime);
        private readonly SqlServerDateTimeTypeMapping _datetime2 = new SqlServerDateTimeTypeMapping("datetime2", dbType: DbType.DateTime2);
        private readonly SqlServerDateTimeOffsetTypeMapping _datetimeoffset = new SqlServerDateTimeOffsetTypeMapping("datetimeoffset");

        private readonly IntTypeMapping _int = new IntTypeMapping("int", DbType.Int32);
        private readonly LongTypeMapping _long = new LongTypeMapping("bigint", DbType.Int64);
        private readonly ShortTypeMapping _short = new ShortTypeMapping("smallint", DbType.Int16);
        private readonly ByteTypeMapping _byte = new ByteTypeMapping("tinyint", DbType.Byte);
        private readonly BoolTypeMapping _bool = new BoolTypeMapping("bit");
        private readonly SqlServerCharTypeMapping _char = new SqlServerCharTypeMapping("char", dbType: DbType.AnsiString);
        private readonly DoubleTypeMapping _double = new SqlServerDoubleTypeMapping("float"); // Note: "float" is correct SQL Server type to map to CLR-type double
        private readonly FloatTypeMapping _real = new SqlServerFloatTypeMapping("real"); // Note: "real" is correct SQL Server type to map to CLR-type float
        private readonly DecimalTypeMapping _decimal = new DecimalTypeMapping("decimal(18, 2)", DbType.Decimal);
        private readonly GuidTypeMapping _uniqueidentifier = new GuidTypeMapping("uniqueidentifier", DbType.Guid);

        private readonly SqlServerObjectTypeMapping _sqlVariant = new SqlServerObjectTypeMapping("sql_variant", dbType: null);

        private readonly HashSet<string> _disallowedMappings;
        private readonly Dictionary<string, TypeMapping> _dbTypeMappings;
        private readonly Dictionary<Type, TypeMapping> _clrTypeMappings;

        public SqlServerTypeMapper(IDataContextOptions contextOptions) : base(contextOptions)
        {
            _internalString = new SqlServerStringTypeMapping("nvarchar", sqlDbType: SqlDbType.NVarChar, unicode: true, options: _contextOptions as DataContextOptions);

            _dbTypeMappings
                = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
                {
                    { "bigint", _long },
                    { "binary varying", _variableLengthBinary },
                    { "binary", _fixedLengthBinary },
                    { "hierarchyid", _hierarchyidBinary },
                    { "geography", _hierarchyidBinary },
                    { "bit", _bool },
                    { "char varying", _variableLengthAnsiString },
                    { "char", _fixedLengthAnsiString },
                    { "character varying", _variableLengthAnsiString },
                    { "character", _fixedLengthAnsiString },
                    { "date", _date },
                    { "datetime", _datetime },
                    { "datetime2", _datetime2 },
                    { "datetimeoffset", _datetimeoffset },
                    { "dec", _decimal },
                    { "decimal", _decimal },
                    { "float", _double },
                    { "image", _variableLengthBinary },
                    { "int", _int },
                    { "money", _decimal },
                    { "national char varying", _variableLengthUnicodeString },
                    { "national character varying", _variableLengthUnicodeString },
                    { "national character", _fixedLengthUnicodeString },
                    { "nchar", _fixedLengthUnicodeString },
                    { "ntext", _ntext },
                    { "numeric", _decimal },
                    { "nvarchar", _variableLengthUnicodeString },
                    { "real", _real },
                    { "rowversion", _rowversion },
                    { "smalldatetime", _datetime },
                    { "smallint", _short },
                    { "smallmoney", _decimal },
                    { "text", _text },
                    { "time", _time },
                    { "timestamp", _fixedLengthBinary },
                    { "tinyint", _byte },
                    { "uniqueidentifier", _uniqueidentifier },
                    { "varbinary", _variableLengthBinary },
                    { "varchar", _variableLengthAnsiString },
                    { "xml", _xml },
                    { "sql_variant", _sqlVariant},
                    { "sysname", _variableLengthUnicodeString}
                };

            // Note: sbyte, ushort, uint, char and ulong type mappings are not supported by SQL Server.
            // We would need the type conversions feature to allow this to work - see https://github.com/aspnet/EntityFramework/issues/242.
            _clrTypeMappings
                = new Dictionary<Type, TypeMapping>
                {
                    { typeof(int), _int },
                    { typeof(long), _long },
                    { typeof(DateTime), _datetime2 },
                    { typeof(Guid), _uniqueidentifier },
                    { typeof(bool), _bool },
                    { typeof(byte), _byte },
                    { typeof(double), _double },
                    { typeof(DateTimeOffset), _datetimeoffset },
                    { typeof(short), _short },
                    { typeof(float), _real },
                    { typeof(decimal), _decimal },
                    { typeof(TimeSpan), _time },
                    { typeof(char), _char },
                    {typeof(object), _sqlVariant}
                };

            // These are disallowed only if specified without any kind of length specified in parenthesis.
            // This is because we don't try to make a new type from this string and any max length value
            // specified in the model, which means use of these strings is almost certainly an error, and
            // if it is not an error, then using, for example, varbinary(1) will work instead.
            _disallowedMappings
                = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "binary varying",
                    "binary",
                    "char varying",
                    "char",
                    "character varying",
                    "character",
                    "national char varying",
                    "national character varying",
                    "national character",
                    "nchar",
                    "nvarchar",
                    "varbinary",
                    "varchar"
                };

            this.ByteArrayMapper
                = new ByteArrayTypeMapper(
                    maxBoundedLength: 8000,
                    defaultMapping: _unboundedBinary,
                    unboundedMapping: _unboundedBinary,
                    keyMapping: _keyBinary,
                    createBoundedMapping: size => new SqlServerByteArrayTypeMapping(
                        "varbinary(" + size + ")",
                        DbType.Binary,
                        size));

            this.StringMapper
                = new StringTypeMapper(
                    maxBoundedAnsiLength: 8000,
                    defaultAnsiMapping: _unboundedAnsiString,
                    unboundedAnsiMapping: _unboundedAnsiString,
                    keyAnsiMapping: _keyAnsiString,
                    createBoundedAnsiMapping: size => new SqlServerStringTypeMapping(
                        "varchar(" + size + ")",
                        unicode: false,
                        size: size),
                    maxBoundedUnicodeLength: 4000,
                    defaultUnicodeMapping: _unboundedUnicodeString,
                    unboundedUnicodeMapping: _unboundedUnicodeString,
                    keyUnicodeMapping: _keyUnicodeString,
                    createBoundedUnicodeMapping: size => new SqlServerStringTypeMapping(
                        "nvarchar(" + size + ")",
                        sqlDbType: null,
                        unicode: true,
                        size: size));
        }

        public override IByteArrayTypeMapper ByteArrayMapper { get; }

        public override IStringTypeMapper StringMapper { get; }

        public override void ValidateTypeName(string dbTypeName)
        {
            if (_disallowedMappings.Contains(dbTypeName))
            {
                throw new ArgumentException(SqlServerStrings.UnqualifiedDataType(dbTypeName));
            }
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }

        public override TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            clrType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType));

            return clrType == typeof(string)
                ? _internalString
                : (clrType == typeof(byte[])
                    ? _unboundedBinary
                    : base.FindMapping(clrType));
        }

    }
}
